package com.cheche.dn.common.base;

import com.cheche.dn.common.Filter;
import com.cheche.dn.domain.admin.Admin;
import com.cheche.dn.domain.admin.Authority;
import com.cheche.dn.domain.admin.Role;
import com.cheche.dn.utils.EntityUtil;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * 简单缓存实体
 * Created by cheshun on 15/11/4.
 */
@Aspect
@Component
public class Cache {

    @Autowired
    private Service service;
    @Autowired
    @Qualifier("commonRedis")
    private RedisTemplate<String, String> commonRedis;
    private long timeout = 2;       // 2hours

    @Pointcut("execution(public * com.cheche.dn.common.base.Service.find(..))")
    public void find() {}

    @Around("find()")
    public Object find(ProceedingJoinPoint pjp) throws Throwable {
        Class c = (Class) pjp.getArgs()[0];
        if (c.equals(Admin.class) || c.equals(Role.class) || c.equals(Authority.class)) {
            return pjp.proceed();
        }
        Object o = pjp.getArgs()[1];
        Object result;
        if (o instanceof Long) {
            Long id = (Long) o;
            String key = buildIdKey(c, id);
            if (commonRedis.hasKey(key)) {
                commonRedis.expire(key, timeout, TimeUnit.HOURS);
                //noinspection unchecked
                return EntityUtil.hashToObject(commonRedis.opsForHash().entries(key), c);
            } else {
                result = pjp.proceed();
                if (result != null) {
                    commonRedis.opsForHash().putAll(key, EntityUtil.objectToStringHash(result));
                    commonRedis.expire(key, timeout, TimeUnit.HOURS);
                    return result;
                }
            }
        }
        if (o instanceof Filter[]) {
            Filter[] filters = (Filter[]) o;
            String key = buildFilterKey(c, filters);
            if (commonRedis.hasKey(key)) {
                commonRedis.expire(key, timeout, TimeUnit.HOURS);
                String id = commonRedis.opsForValue().get(key);
                return service.find(c, Long.valueOf(id));
            } else {
                result = pjp.proceed();
                if (result != null) {
                    IdEntity entity = (IdEntity) result;
                    commonRedis.opsForValue().set(key, entity.getId().toString(), timeout, TimeUnit.HOURS);
                    return result;
                }
            }
        }
        return null;
    }

    @Pointcut("execution(public * com.cheche.dn.common.base.Service.*pdate(..))")
    public void update() {}

    @AfterReturning(pointcut="update()", returning="retVal")
    public Object update(Object retVal) {
        Class c = retVal.getClass();
        if (c.equals(Admin.class) || c.equals(Role.class) || c.equals(Authority.class)) {
            return retVal;
        }
        Long id = ((IdEntity) retVal).getId();
        String key = buildIdKey(c, id);
        commonRedis.opsForHash().putAll(key, EntityUtil.objectToStringHash(retVal));
        commonRedis.expire(key, timeout, TimeUnit.HOURS);
        deleteFilterKey(c);
        return retVal;
    }

    @Pointcut("execution(public * com.cheche.dn.common.base.Service.delete(..))")
    public void delete() {}

    @AfterReturning(pointcut = "delete()")
    public void delete(JoinPoint jp) {
        Object[] args = jp.getArgs();
        if (args.length == 1) {
            Class c = args[0].getClass();
            deleteFilterKey(c);
            Long id = ((IdEntity) args[0]).getId();
            String key = buildIdKey(c, id);
            commonRedis.delete(key);
        } else if (args.length == 2) {
            Class c = (Class) args[0];
            deleteFilterKey(c);
            if (args[1] instanceof Long) {
                Long id = (Long) args[1];
                String key = buildIdKey(c, id);
                commonRedis.delete(key);
            } else if (args[1] instanceof Long[]) {
                Long[] ids = (Long[]) args[1];
                for (Long id : ids) {
                    String key = buildIdKey(c, id);
                    commonRedis.delete(key);
                }
            }
        }
    }

    @Pointcut("execution(public * com.cheche.dn.common.base.Service.exists(..))")
    public void exist() {}

    @Around("exist()")
    public Object exist(ProceedingJoinPoint pjp) throws Throwable {
        Class c = (Class) pjp.getArgs()[0];
        if (c.equals(Admin.class) || c.equals(Role.class) || c.equals(Authority.class)) {
            return pjp.proceed();
        }
        Object o = pjp.getArgs()[1];
        if (o instanceof Long) {
            Long id = (Long) o;
            String key = buildIdKey(c, id);
            if (commonRedis.hasKey(key)) {
                return true;
            }
        } else if (o instanceof Filter[]) {
            Filter[] filters = (Filter[]) o;
            String key = buildFilterKey(c, filters);
            if (commonRedis.hasKey(key)) {
                String v = commonRedis.opsForValue().get(key);
                if (commonRedis.hasKey(buildIdKey(c, Long.valueOf(v)))) {
                    return true;
                }
            }
        }
        return pjp.proceed();
    }

    private String buildIdKey(Class c, Long id) {
        return c.getName() + ":id:" + id;
    }

    private String buildFilterKey(Class c, Filter[] filters) {
        String key;
        StringBuilder builder = new StringBuilder(c.getName()).append(":filter:");
        if (filters.length == 1) {
            Filter filter = filters[0];
            builder.append(filter.getProperty()).append(filter.getOperator()).append(filter.getValue()).append(":");
            key = builder.toString();
        } else {
            Map<String, Filter> filterMap = new HashMap<>(filters.length);
            for (Filter filter : filters) {
                filterMap.put(filter.getProperty() + filter.getOperator(), filter);
            }
            for (String fKey : filterMap.keySet()) {
                Filter filter = filterMap.get(fKey);
                builder.append(fKey).append(filter.getValue()).append(":");
            }
            key = builder.toString();
        }
        return key;
    }

    private void deleteFilterKey(Class c) {
        // 删除所有条件key
        Set<String> filterKeys = commonRedis.keys(c.getName() + ":filter:*");
        for (String filterKey : filterKeys) {
            commonRedis.delete(filterKey);
        }
    }
}
